避坑 | 早跟你说了不要写 hardcode!
什么是 hardcode?
// 第三方接口的 IP 地址
private static final IP = 10.10.10.1;
void doPost() {
// 得到请求地址
String requestUrl = IP + '/getUser';
// 发送请求
...
}
看似没什么问题,但是如果要请求的接口地址发生变化,而业务负责人没有收到通知或者未即时修改,会导致所有的请求失败!
2. 写死逻辑,比如在查询火车票价格时,针对特定 uid 的用户(比如某大客户)返回特殊的促销票价:
// 特殊用户直接返回票价
if (uid == 10001) {
return 50;
} else {
// 从数据库查询票价
return db.getPriceByUid(uid);
}
这里其实有两个 hardcode,"10001" 和 "50"。如果后续该用户的特殊身份失效,或者票价发生变化,又或者需要再给一批 uid 添加特权,都需要改动代码,重新上线(在大公司,代码不可以随便发布,通常要通过审批等流程),非常麻烦!
以上好像还不能看出 hardcode 的危害有多大,如果项目代码都在自己的掌控之下,大不了数值变化时改代码呗。但是在大公司,或者人数众多的项目团队中,可能有些 hardcode 没那么容易被发现,hardcode 带来的影响和危害可以说是致命的!
下面鱼皮用自己悲伤的加班经历告诉大家,为什么不要写 hardcode。
一行 hardcode 助我加班
背景
公司内部有一个知名的消息队列服务,鱼皮负责的项目中用到了消息队列生产端进行消息的推送,供其他业务方订阅消息进行独立处理。
一般情况下,连接消息队列就和连接数据库一样,输入地址、账号、密码,直连消息队列生产者即可,如图:
但是有时我们不直接连接消息队列本身,而是连接代理,由代理负责将要发送的消息推送到不同的消息队列生产端,再进行发送,可以实现负载均衡。此时在代码中配置的地址是代理的地址而不是某一个具体的消息生产者,如图:
由于数据量很大,我们使用第二种模式,在代码中连接生产者代理,但之前代理使用的是固定的 IP 地址,代码如下:
final String IP = "10.10.10.2";
MessageProducer mp = new MessageProducer(IP, "xx", "xx");
固定 IP 毫无疑问是个 hardcode。但打死也没想到,这段不起眼的小 hardcode,给鱼皮带来了巨大的工作量!
风云突变
前几天,鱼皮正在恰可乐,该消息队列的服务团队突然发了个公告,内容如下:
这时,鱼皮才想起来那段 hardcode,改吧改吧。。。
下面是一顿操作:
1. 先在代码中把 IP 地址改为他们指定的域名
2. 用脚本检测我们线上机器到这个域名的网络连通性,千万别 ping 不通!
3. 确认连通性后,部署服务到测试环境,进行消息推送测试
4. 测试通过后,提交代码,等审批
5. 审批之后,灰度部署(只升级部分机器)
6. 观察无问题,全量部署
搞定?
深藏不露
如果这样就搞定了,那真是太谢天谢地了。
改 bug 不可怕,可怕的是,你不知道有没有 bug,bug 藏在什么地方。。。
原来所有使用这个消息队列的代码都配置的固定 IP,而此时鱼皮只是更换了自己负责的业务代码中的 IP 地址,但是我们依赖的其他服务(或者是一个 jar 包),有没有使用到这个代理,从而引入了 hardcode 呢?必须要全面且仔细地排查!
通过 gradle build --scan 命令扫描项目依赖的 jar 包,排查有没有这个消息队列的连接包。
经过排查,还真有一个 jar 包,使用该消息队列进行监控数据上报。在这个 jar 包中,静静地躺着似曾相识的 hardcode:
final String IP = "10.10.10.2";
MessageProducer mp = new MessageProducer(IP, "xx", "xx");
于是,鱼皮将 IP 切换成域名,然后升级包的版本,在依赖中使用新版本 jar 包。
搞定 x 2?
慈航普渡
此时,鱼皮只是解决了自己业务中的 hardcode,但是,使用这个包含过期 IP 的 jar 包的项目非常多,如果这些项目不对 jar 包进行升级的话,几天后用到这个 jar 包的项目都会受到影响!
既然看到了,不能不管吧!
于是,鱼皮调查了所有可能使用这个 jar 包的团队,然后给他们发送了升级该 jar 的通知,这才终于搞定了!下班下班。
没想到,一行 hardcode 就带来了这么大的风险和工作量,善哉善哉。
既然 hardcode 危害那么大,有什么办法避免写 hardcode 呢?
如何避免 hardcode?
针对不同的 hardcode,有不同的处理方式,此处总结如下几个技巧。
1. 变量引用
最简单的做法,就是为相同的固定值定义一个变量(常量),最好在单独的类或文件中。这样,在修改时,只要修改一处即可。
错误代码:
String requestUrl1 = "10.1.1.1" + "/getUser";
String requestUrl2 = "10.1.1.1" + "/getSku";
String requestUrl3 = "10.1.1.1" + "/getOrder";
正确代码:
final String IP = "10.1.1.1";
String requestUrl1 = IP + "/getUser";
String requestUrl2 = IP + "/getSku";
String requestUrl3 = IP + "/getOrder";
如果要调用第三方接口,最好不要在代码中直接写死 IP 地址,而是使用域名作为地址调用,可以通过在机器配置 host 来改变实际的服务 IP,便于维护。
2. 配置化
将所有可能改动的信息(比如数据库账号、密码、服务端口)单独存放到配置文件中进行管理,通过读取配置来获取信息。修改信息时只需要独立修改配置文件,而不用改动代码,非常方便,是目前项目开发中最常使用的一种方式。
示例配置文件:
server.ip = 10.1.1.0
server.port = 8080
db.username = yupi
db.password = yupi
读取配置文件的代码:
String serverIp = Config.getConfig("server.ip");
3. 动态配置
通过配置化的方式已经能够解决项目中的 hardcode,但还有一个问题,就是改动配置后,项目可能需要重新启动才能让新配置生效,还不够灵活。
为此,我们可以利用分布式配置中心实现动态配置,将所有的配置存放在数据库或分布式缓存、Etcd 中,通过在业务代码中引入 SDK 来监听配置变量。当配置发生修改时,变量的值会同步进行修改,而无需重启项目。
现在比较主流的配置中心有携程的 Apollo、阿里 Nacos 和 Spring Cloud Config 等,很多配置中心提供了可视化的界面,可以方便地进行配置管理、修改发布和版本控制。
以上就是关于为什么不要写 hardcode 以及如何避免写 hardcode 的全部内容啦。
如果觉得有帮助,记得分享给小伙伴,再写硬代码头打烂!